package servers;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
 
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import shared.*;

public class Server implements ServerRemoteInterface 
{
	
	private HashMap<String, Stock> stockList;
	private HashMap<String, User>  userList;
	private Thread thread;
	private boolean isThreadOver = false;
	
	private String stockFileName = "stocksData.xml";
	private String userFileName="usersData.xml";
	
	public String test(){
		return "Call is successful!\n";
	}
	
	public Server() 
	{
		super();
		
		this.initializeStockList();
		this.initializeUserList();
		thread = new Thread(new updateListThread());
		thread.start();
	}
	
	
	public Stock queryStock(String stockName) throws RemoteException 
	{
		stockName = stockName.toUpperCase();
		Stock stock = this.stockList.get(stockName);
		if (stock == null)
		{
			Status status = this.addStock(stockName);
			if (status.getCode() != 0){
				return null;
			}
			stock = this.stockList.get(stockName);
		}
		
		return stock;
	}
	
	public ArrayList<Stock> getAllStocks() throws RemoteException
	{
		return new ArrayList<Stock>(this.stockList.values());
	}
	

	
	public synchronized Status buy(String stockName, int number, String userName) throws RemoteException 
	{	stockName = stockName.toUpperCase();
		Status status = new Status();
		Stock stock = this.stockList.get(stockName);
		if (stock == null)
		{
			status = this.addStock(stockName);
			if (status.getCode() != 0){
				return status;
			}
			stock = this.stockList.get(stockName);
		}
		
		
		User user = this.getUser(userName);
		int numberLeft = stock.getNumberOfShares();
		double balance = user.getBalance();
			
		if (number > numberLeft){
			status.setCode(100);
			status.setMessage("No enough share available to buy!");
			number = numberLeft;
			return status;
		}
		if (number*stock.getPrice() > balance)
			{number = (int) (balance/stock.getPrice());
			status.setCode(100);
			status.setMessage("No enough balance to buy!");
			return status;
			}
		
		stock.setNumberOfShares(stock.getNumberOfShares()-number);
		
		this.stockList.put(stockName, stock);
		
		
		user.setBalance(user.getBalance() - number*stock.getPrice());	
		
		Stock userStock = user.stockList.get(stockName);
		if ( userStock != null )
			userStock.setNumberOfShares(userStock.getNumberOfShares() + number);
		else
		{
			userStock = new Stock();
			userStock.setName(stockName);
			userStock.setExchange("NASDAQ");
			userStock.setNumberOfShares(number);
			userStock.setPrice(stock.getPrice());
		}
		user.stockList.put(stockName, userStock);
		this.userList.put(userName, user);
		
		status.setCode(0);
		status.setMessage("Successfully bought "+number +" shares");
		return status;
	}
	
	

	public synchronized Status sell(String stockName, int number, String userName) throws RemoteException
	{	
		stockName = stockName.toUpperCase();
		Status status = new Status();
		Stock stock = this.stockList.get(stockName);
		if (stock == null)
		{
			status = this.addStock(stockName);
			if(status.getCode() != 0){
				return status;
			}
			stock = this.stockList.get(stockName);
		}
		
		
		User user = this.getUser(userName);
		Stock userStock = user.stockList.get(stockName);
		if (userStock == null){
			status.setCode(300);
			status.setMessage("User does have stock !");
			return status;
		}
		int numberLeft = userStock.getNumberOfShares();
				
		if (number > numberLeft)
			number = numberLeft;

		
		stock.setNumberOfShares(stock.getNumberOfShares() + number);
		this.stockList.put(stockName, stock);
		
		user.setBalance(user.getBalance() + number*stock.getPrice());
		userStock.setNumberOfShares(numberLeft - number); 
		user.stockList.put(stockName, userStock);
		if(userStock.getNumberOfShares()==0){
			user.stockList.remove(stockName);
		}
		this.userList.put(userName, user);
		
		
		status.setCode(0);
		status.setMessage("Successfully sold "+number +" shares");
		return status;
	}
	
	

	public synchronized Status modifyStockPrice(String stockName, double newPrice)
			throws RemoteException 
	{
		Status status = new Status();
		
		Stock stock = stockList.get(stockName);
		if(stock == null)
		{
			status.setCode (200);
			status.setMessage("Stock Name does not exist in server");
			return status;
		}
		stock.setPrice(newPrice);
		stockList.put(stockName, stock);
		status.setMessage("Modify Successful");
		return status;
	}
	
	
	
	
	
	class updateListThread implements Runnable
	{
		@Override
		public void run() 
		{
			while (!isThreadOver)
			{
				updateStockList();
				saveStockList();
				saveUserList();
				
				try 
				{
					Thread.sleep(1000 * 60 * 2);
				} 
				catch (InterruptedException e) 
				{
					//e.printStackTrace();
				}
			}
			
			return;
		}

	}
	
	
	private synchronized void initializeUserList()
	{
		userList = new HashMap<String, User>();
		
		// populate userList, read from XML file
		File userFile = new File(this.userFileName);
		
		if (!userFile.canRead())
		{
			System.err.println("Failed to open user file!");
			return;
		}
		
		
		try{
			DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
			Document doc = dBuilder.parse(userFile);
			doc.getDocumentElement().normalize();
	 
			NodeList nodeList = doc.getElementsByTagName("User");
			for (int i = 0; i<nodeList.getLength(); i++){
				Element e = (Element) nodeList.item(i);
				User newUser = new User();
				
				newUser.setName( e.getAttribute("name"));
				newUser.setBalance( Double.parseDouble(e.getAttribute("balance")));
				
				//read the user stocks
				NodeList sList = e.getElementsByTagName("userStock");
				for (int j = 0; j<sList.getLength(); j++){
					Element us = (Element) sList.item(j);
					Stock newStock = new Stock();
					
					newStock.setName( us.getAttribute("name"));
					newStock.setPrice( Double.parseDouble(us.getAttribute("price")));
					newStock.setExchange( us.getAttribute("exchange"));
					newStock.setNumberOfShares(Integer.parseInt( us.getAttribute("numberOfShares")));
					newUser.stockList.put(newStock.getName(), newStock);
					System.out.println(newStock);
				}
				
				this.userList.put(newUser.getName(), newUser);
				//System.out.println(newUser);
			}
			//read over
			System.out.println("Read " +this.userList.size() +
					" user information from file: "+this.userFileName);
			}
			catch (Exception e){
				e.printStackTrace();
			}
		
		
		
		
	}
	
	
	private synchronized void initializeStockList()
	{
		
		stockList = new HashMap<String, Stock>();
		// populate userList, read from XML file
		File stockFile = new File(this.stockFileName);
		
		if (!stockFile.canRead())
		{
			System.err.println("Failed to open stock file!");
			return;
		}
		try{
		DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
		Document doc = dBuilder.parse(stockFile);
		doc.getDocumentElement().normalize();
 

		NodeList nodeList = doc.getElementsByTagName("Stock");
		for (int i = 0; i<nodeList.getLength(); i++){
			Element e = (Element) nodeList.item(i);
			Stock newStock = new Stock();
			
			newStock.setName( e.getAttribute("name"));
			newStock.setPrice( Double.parseDouble(e.getAttribute("price")));
			newStock.setExchange( e.getAttribute("exchange"));
			newStock.setNumberOfShares(Integer.parseInt( e.getAttribute("numberOfShares")));
			this.stockList.put(newStock.getName(), newStock);
			//System.out.println(newStock);
		}
		//read over
		System.out.println("Read " +this.stockList.size() +
				" stocks information from file: "+this.stockFileName);
		}
		catch (Exception e){
			e.printStackTrace();
			System.err.println("File: "+this.userFileName+" is mal-formatted!! cann't read any information");
			return;
		}
		
	}

	
	
	private void updateStockList()
	{
		HashMap<String, Stock> tempList = (HashMap<String, Stock>)stockList.clone();
		// update tempList
		for (Stock stock : tempList.values())
		{
			this.updateStock(stock);
		}
		this.overwriteStockList(tempList);
	}
	
	
	private synchronized void overwriteStockList(HashMap<String, Stock> list)
	{
		this.stockList = list;
	}
	
	

	public User getUser(String userName) throws RemoteException
	{
		User user = userList.get(userName);
		if (user == null)
		{
			user = new User();
			user.setName(userName);
			user.setBalance(1000.00);
			userList.put(userName, user);
		}
		return user;

	}


	
	public synchronized Status addStock(String stockName)
	{
		stockName = stockName.toUpperCase();
		Status status = new Status();
		
		Stock stock = new Stock();
		stock.setName(stockName);
		stock.setNumberOfShares(1000);
		stock.setPrice(1.0);
		stock.setExchange("NASDAQ");
		
		
		status =this.updateStock(stock);
		if (status.getCode() != 0){
			return status;
		}
		this.stockList.put(stockName, stock);
		return status;
	}
	
	
	private synchronized void saveUserList()
	{

		try {
			DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder docBuilder;
			docBuilder = docFactory.newDocumentBuilder();
			
			// root elements
			Document doc = docBuilder.newDocument();
			Element rootElement = doc.createElement("Users");
			doc.appendChild(rootElement);
			
			for (User user: this.userList.values()){
				Element userEntry = doc.createElement("User");
				userEntry.setAttribute("name", user.getName());
				userEntry.setAttribute("balance", user.getBalance()+"");
				
				//write users' stock list
				for (Stock stock: user.stockList.values()){
					Element userStock = doc.createElement("userStock");
					userStock.setAttribute("name", stock.getName());
					userStock.setAttribute("price", stock.getPrice()+"");
					userStock.setAttribute("exchange", stock.getExchange());
					userStock.setAttribute("numberOfShares", stock.getNumberOfShares()+"");
					userEntry.appendChild(userStock);
				}
				
				
				rootElement.appendChild(userEntry);
			}
			
			
			//write to disk
			TransformerFactory transformerFactory = TransformerFactory.newInstance();
			Transformer transformer = transformerFactory.newTransformer();
			DOMSource source = new DOMSource(doc);
			StreamResult result = new StreamResult(new File(this.userFileName));
	 
			transformer.transform(source, result);
	 
			System.out.println("User Information Saved");
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	
	private synchronized void  saveStockList()
	{

		try {
			DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
			DocumentBuilder docBuilder;
			docBuilder = docFactory.newDocumentBuilder();
			
			// root elements
			Document doc = docBuilder.newDocument();
			Element rootElement = doc.createElement("stocks");
			doc.appendChild(rootElement);
			
			for (Stock stock: this.stockList.values()){
				Element stockEntry = doc.createElement("Stock");
				stockEntry.setAttribute("name", stock.getName());
				stockEntry.setAttribute("price", stock.getPrice()+"");
				stockEntry.setAttribute("exchange", stock.getExchange());
				stockEntry.setAttribute("numberOfShares", stock.getNumberOfShares()+"");
				rootElement.appendChild(stockEntry);
			}
			
			
			//write to disk
			TransformerFactory transformerFactory = TransformerFactory.newInstance();
			Transformer transformer = transformerFactory.newTransformer();
			DOMSource source = new DOMSource(doc);
			StreamResult result = new StreamResult(new File(this.stockFileName));
	 
			transformer.transform(source, result);
	 
			System.out.println("Stock Information Saved");
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
 
		
	}
	
	
	
	public Status updateStock(Stock stock)
	{
		Status status= new Status();
		String stockName      = stock.getName();
		String stockExchange  = stock.getExchange();
		
		URL apiURL = null;
		
		try 
		{
			apiURL = new URL("http://finance.google.com/finance/info?client=ig&q=" + stockExchange + ":" + stockName);
			//apiURL = new URL("http://finance.google.com/finance/info?client=ig&q=NASDAQ%3aGOOG");
		} 
		catch (MalformedURLException e) 
		{
			status.setCode(111);
			status.setMessage(e.getMessage());
			e.printStackTrace();
			return status;
			
		}
		
		if (apiURL == null)
		{
			System.err.println("Failed to open URL to Google Finance API!");
			status.setCode(111);
			status.setMessage("Failed to open URL to Google Finance API!");
			return status;
		}
		
		BufferedReader reader = null;
		
		try 
		{
			reader = new BufferedReader(new InputStreamReader(apiURL.openStream()));
		} 
		catch (IOException e) 
		{
			System.err.println("Failed to open URL to Google Finance API!");
			status.setCode(111);
			status.setMessage("Failed to open URL to Google Finance API!");
			return status;
		}
		
		
		Scanner scanner = new Scanner(reader);
		if (!scanner.hasNext()){
			//empty returned
			status.setCode(211);
			status.setMessage("Could not find the thicker symbol or exchange!!");
			return status;
		}
			
			
		double newPrice = 0.0;
		System.out.println("Old price: " + String.format("%.2f", stock.getPrice()));
		
		while (scanner.hasNext())
		{
			String token = scanner.next();
			if (token.contains("l_cur"))
			{
				token = scanner.next();
				token = scanner.next();
				
				if (token.length() > 2)
					token = token.substring(1, token.length()-1);
				try
				{
					newPrice = Double.parseDouble(token);
					if (newPrice == 0.0)
						throw new Exception();
					stock.setPrice(newPrice);
					break;
				}
				catch (Exception e)
				{
					
					System.err.println("Error parsing price");
					status.setCode(200);
					status.setMessage("Wrong");
					return status;
				}
			}
		}
		System.out.println("New Price: " + String.format("%.2f", stock.getPrice()));
		return status;
	}
	
	
	
	public void close()
	{	
		System.out.println("Server is closing");
		this.isThreadOver = true;
		try 
		{
			thread.interrupt();
			thread.join();
		} 
		catch (InterruptedException e) 
		{
			// TODO Auto-generated catch block
			//e.printStackTrace();
		}		
		
		this.saveStockList();
		this.saveUserList();
		System.out.println("Server is closed");
		System.exit(0);
	}
	

	public static void main(String[] args)
	{
		String rmiObjectName = "StockServer";
		//String rmiObjectName = args[0];
		//Server server = new Server();
		//server.updateStock(new Stock());
		
		//setup rmi connection
		ServerRemoteInterface myInterface=null;
		 if (System.getSecurityManager() == null) 
		 {
			 System.setSecurityManager(new SecurityManager());
	     }
	        
		 try 
		 {
	     
			myInterface = new Server();
			ServerRemoteInterface stub = (ServerRemoteInterface) UnicastRemoteObject.exportObject(myInterface, 0);
			Registry registry = LocateRegistry.getRegistry();
			registry.rebind(rmiObjectName, stub);
			System.out.println("Stock Server bound, type quit to close the server");
		 } 
		 catch (Exception e) 
		 {
			System.err.println("Fail to initialize RMI connection: ");
			e.printStackTrace();
			return;
		}
		 
		 Scanner input = new Scanner(System.in);
		 String line = input.nextLine();
		 while (!line.equalsIgnoreCase("quit"))
		 {	
			 System.out.println("Type quit to close the server");
			 line = input.nextLine();
		 }
		 
		 Server me = (Server)myInterface;
		 me.close();
	}
	
	

}
